package com.seckill.service.impl;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;

import com.seckill.dao.SeckillDao;
import com.seckill.dao.SuccessKilledDao;
import com.seckill.dao.cache.RedisDao;
import com.seckill.dto.Exposer;
import com.seckill.dto.SeckillExecution;
import com.seckill.entity.Seckill;
import com.seckill.entity.SuccessKilled;
import com.seckill.enums.SeckillStatEnum;
import com.seckill.exception.RepeatKillException;
import com.seckill.exception.SeckillCloseException;
import com.seckill.exception.SeckillException;
import com.seckill.service.SeckillService;

@Service
public class SeckillServiceImpl implements SeckillService {
	// 日志
	private Logger logger = LoggerFactory.getLogger(this.getClass());

	// MD5盐值字符串，用于混淆MD5
	private final String slat = "sSDf23!@#4SDFa2dsf34sF4";

	@Autowired
	private SeckillDao seckillDao;

	@Autowired
	private RedisDao redisDao;

	@Autowired
	private SuccessKilledDao successKilledDao;

	/**
	 * 根据给定的偏移量和limit查询所有的秒杀
	 */
	public List<Seckill> getSeckillList(int offset, int limit) {
		return seckillDao.queryAll(offset, limit);
	}

	/**
	 * 通过id查询一个秒杀
	 */
	public Seckill getById(Long seckillId) {
		return seckillDao.queryById(seckillId);
	}

	/**
	 * 秒杀接口
	 */
	public Exposer exportSeckillUrl(Long seckillId) {

		// 缓存优化 : 通过设置超时穿透来维护一致性
		// 一般秒杀商品不会修改 发现错误删除它重新建立一个 所以可以通过设置超时穿透
		Seckill seckill = redisDao.getSeckillRedis(seckillId);
		if (seckill == null) {
			seckill = seckillDao.queryById(seckillId);
			if (seckill == null) {
				return new Exposer(false, seckillId);
			} else {
				redisDao.setSeckillRedis(seckill);
			}
		}

		// 秒杀存在 判断时间
		Date startTime = seckill.getStartTime();
		Date endTime = seckill.getEndTime();
		Date now = new Date();

		// 当前时间小于开启时间或者大于结束时间
		if (now.getTime() < startTime.getTime() || now.getTime() > endTime.getTime()) {
			return new Exposer(false, seckillId, now.getTime(), startTime.getTime(), endTime.getTime());
		}

		// 条件符合 可以秒杀
		String md5 = getMD5(seckillId);
		return new Exposer(true, md5, seckillId);
	}

	public String getMD5(Long seckillId) {
		String base = seckillId + "/" + slat;
		String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
		return md5;
	}

	/**
	 * 执行秒杀 使用注解控制事物方法的优点 开发团队可以形成约定，明确标注事物方法的编程风格。 保证事物的执行时间尽可能短，不要穿插其他的网络操作。
	 * 不是所有的方法都需要事物，比如只有一条修改操作或者之都操作。
	 */
	@Transactional
	public SeckillExecution executeSeckill(Long seckillId, Long userPhone, String md5)
			throws SeckillException, SeckillCloseException, RepeatKillException {
		if (md5 == null || md5.length() == 0 || !getMD5(seckillId).equals(md5)) {
			throw new SeckillException("seckill data rewrite !");
		}

		Date nowTime = new Date();

		try {
			int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
			if (updateCount <= 0) {
				// 表示减库存失败 秒杀关闭
				throw new SeckillCloseException("seckill is closed");
			} else {
				// 减库存成功，向successSeckill插入一行记录
				int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
				// 如果插入失败 表示重复秒杀
				if (insertCount <= 0) {
					throw new RepeatKillException("seckill repeat");
				} else {
					// 成功
					SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
					return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
				}
			}
		} catch (SeckillCloseException e) {
			throw e;
		} catch (RepeatKillException e) {
			throw e;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new SeckillException("seckill inner error" + e.getMessage());
		}
	}

	public SeckillExecution executeSeckillProcedure(Long seckillId, Long userPhone, String md5) {
		if (md5 == null || md5.length() == 0 || !getMD5(seckillId).equals(md5)) {
			throw new SeckillException("seckill data rewrite !");
		}
		Date killTime = new Date();
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("seckillId", seckillId);
		map.put("phone", userPhone);
		map.put("killTime", killTime);
		map.put("result", null);
		try {
			seckillDao.killByProcedure(map);
			int result = MapUtils.getInteger(map, "result", -2);
			if (result == 1) {
				SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
				return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
			} else {
				return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
		}
	}
}
